package com.unitd.frame.sso.service.sso.impl;

import com.unitd.frame.sso.common.config.SSOConfig;
import com.unitd.frame.sso.common.helper.CookieHelper;
import com.unitd.frame.sso.common.helper.IpHelper;
import com.unitd.frame.sso.common.token.Token;
import com.unitd.frame.sso.common.util.BrowserUtil;
import com.unitd.frame.comm.utils.RandomUtil;
import com.unitd.frame.sso.constant.TokenConstant;
import com.unitd.frame.sso.exception.SsoException;
import com.unitd.frame.sso.service.cache.ISSOCache;
import com.unitd.frame.sso.service.encrypt.ISSOEncrypt;
import com.unitd.frame.sso.service.plugin.AbstractSSOPlugin;
import com.unitd.frame.sso.service.statistic.ISSOStatistic;
import com.unitd.frame.comm.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

/**
 * @desc SSO 单点登录服务支持类
 * @filename SSOServiceSupport.java
 * @copyright www.unitd.com
 * @author Hudan
 * @version 1.0
 * @date 2016/10/14
 */
public class SSOServiceSupport {

	protected final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
	protected SSOConfig config;

	public SSOConfig getConfig() {
		return config;
	}

	public void setConfig(SSOConfig config) {
		this.config = config;
	}

	/* ************************** 登录/登出/统计相关处理方法 ************************** */
	/**
	 * @desc 设置当前访问域下的登录Cookie
	 * @param request  http请求对象
	 * @param response http响应对象
	 * @param encrypt  对称加密算法类
	 */
	protected void setSSOCookie(HttpServletRequest request, HttpServletResponse response, Token token,
								ISSOEncrypt encrypt) {
		if (encrypt == null) {
			throw new SsoException("对称加密算法类不能为空,请先指定对应的加密算法!");
		}
		try {
			/* 根据Token生成登录信息Cookie */
			Cookie ck = generateCookie(request, token, encrypt);

			/* 判断 Token 是否缓存处理失效; cache缓存宕机,flag设置为失效 */
			ISSOCache cache = config.getCache();
			if (cache != null) {
				boolean rlt = cache.set(token.toCacheKey(), token, config.getCacheExpires());
				if (!rlt) {
					token.setFlag(TokenConstant.FLAG_CACHE_SHUT);
				}
			}

			/* 在线人数统计 +1 */
			ISSOStatistic statistic = config.getStatistic();
			if (statistic != null) {
				boolean rlt = statistic.increase(request);
				if (!rlt) {
					statistic.increase(request);
				}
			}

			/* 执行插件逻辑 */
			List<AbstractSSOPlugin> pluginList = config.getPluginList();
			if (pluginList != null) {
				for (AbstractSSOPlugin plugin : pluginList) {
					boolean login = plugin.login(request, response);
					if (!login) {
						plugin.login(request, response);
					}
				}
			}

			/* Cookie设置HttpOnly */
			if (config.getCookieHttponly()) {
				CookieHelper.addHttpOnlyCookie(response, ck);
			} else {
				response.addCookie(ck);
			}
		} catch (Exception e) {
			logger.error("设置cookie的HTTPOnly并创建AUI出现异常:" + e.toString());
		}
	}

	/**
	 * @desc 退出当前登录状态
	 * @param request  http请求对象
	 * @param response http响应对象
	 * @param cache    ISSOCache
	 * @return boolean true 成功, false 失败
	 */
	protected boolean logout(HttpServletRequest request, HttpServletResponse response, ISSOCache cache) {
		/* Token 如果开启了缓存,删除缓存记录 */
		if (cache != null && !SSOConfig.SSO_KICK_USER.equals(request.getAttribute(SSOConfig.SSO_KICK_FLAG))) {
			Token tk = getTokenFromCookie(request);
			if (tk != null) {
				boolean rlt = cache.delete(tk.toCacheKey());
				if (!rlt) {
					cache.delete(tk.toCacheKey());
				}
			}
		}

		/* 在线人数统计 -1 */
		ISSOStatistic statistic = config.getStatistic();
		if (statistic != null) {
			boolean rlt = statistic.decrease(request);
			if (!rlt) {
				statistic.decrease(request);
			}
		}

		/* 执行插件逻辑 */
		List<AbstractSSOPlugin> pluginList = config.getPluginList();
		if (pluginList != null) {
			for (AbstractSSOPlugin plugin : pluginList) {
				boolean logout = plugin.logout(request, response);
				if (!logout) {
					plugin.logout(request, response);
				}
			}
		}

		/* 删除登录 Cookie */
		return CookieHelper.clearCookieByName(request, response, config.getCookieName(), config.getCookieDomain(),
				config.getCookiePath());
	}

	/**
	 * @desc 校验 Token 中的 IP 与登录 IP 是否一致
	 * @param request http请求对象
	 * @param token   登录票据
	 * @return Token {@link Token}
	 */
	protected Token checkIp(HttpServletRequest request, Token token) {
		// 判断是否开启了 Cookie 的 IP 一致性校验
		if (config.getCookieCheckip()) {
			// 获取当前请求的IP地址
			String requestIP = IpHelper.getIpAddr(request);

			// 当当前请求的IP与Token中的IP不一致时,直接返回null,否则返回当前请求的Token
			if (token != null && StringUtils.isNotBlank(requestIP) && !requestIP.equals(token.getIp())) {
				logger.info(String.format("登录IP与Token中的IP不一致,返回标记为空.令牌Ip: % s,请求IP:%s",
						token.getIp(), requestIP));
				return null;
			}
		}
		return token;
	}


	/* ************************** 客户端处理Token相关方法 ************************** */
	/**
	 * @desc 获取当前请求 Token
	 * @param request 访问请求
	 * @param encrypt 对称加密算法类
	 * @param cache   SSO缓存
	 * @return 当前请求的具体 Token {@link Token}
	 */
	protected Token getToken(HttpServletRequest request, ISSOEncrypt encrypt, ISSOCache cache) {
		if (encrypt == null) {
			throw new SsoException("加密对象不能为空!");
		}

		// 校验Token IP 与登录 IP 是否一致
		Token token = checkIp(request, cacheToken(request, encrypt, cache));

		// 执行插件逻辑
		List<AbstractSSOPlugin> pluginList = config.getPluginList();
		if (pluginList != null) {
			for (AbstractSSOPlugin plugin : pluginList) {
				// 验证 Token 的合法性
				boolean valid = plugin.validateToken(token);
				if (!valid) {
					return null;
				}
			}
		}
		return token;
	}

	/**
	 * @desc 从请求中获取当前请求Token
	 * 由于在登录拦截器中已存放了解密之后的请求Token信息,所以在这里这几获取即可,防止二次解密
	 * 此属性在过滤器拦截器中设置,业务系统中调用有效
	 * @param request 访问请求
	 * @return 当前请求的具体 Token {@link Token}
	 */
	@SuppressWarnings("unchecked")
	public <T extends Token> T attrToken(HttpServletRequest request) {
		return (T) request.getAttribute(SSOConfig.SSO_TOKEN_ATTR);
	}

	/**
	 * @desc 获取当前请求 JsonToken
	 * @param request    http请求对象
	 * @param encrypt    对称加密算法类
	 * @param cookieName Cookie名称
	 * @return String 当前Token的json格式值
	 */
	protected String getJsonToken(HttpServletRequest request, ISSOEncrypt encrypt, String cookieName) {
		Cookie uid = CookieHelper.findCookieByName(request, cookieName);
		if (uid != null) {
			String jsonToken = uid.getValue();
			String[] tokenAttr = new String[2];
			try {
				jsonToken = encrypt.decrypt(jsonToken, config.getSecretkey());
				tokenAttr = jsonToken.split(SSOConfig.CUT_SYMBOL);
			} catch (Exception e) {
				logger.error("jsonToken解密错误,可能是非法的登录. 登录IP = " + IpHelper.getIpAddr(request));
			}
			// 判断是否认证浏览器混淆信息
			if (config.getCookieBrowser()) {
				if (BrowserUtil.isLegalUserAgent(request, tokenAttr[0], tokenAttr[1])) {
					return tokenAttr[0];
				} else {
					// 签名验证码失败
					logger.error("验证签名失败,登录的浏览器非法或者未正常登录!");
				}
			} else {
				// 不需要认证浏览器信息混淆返回JsonToken
				return tokenAttr[0];
			}
		}

		return null;
	}

	/**
	 * @desc 从 Cookie 中获取 Token ,此方法未验证 IP 等其他信息(1、自动设置;2、拦截器 request 中获取;3、解密 Cookie 获取)
	 * @param request http请求对象
	 * @return Token {@link Token}
	 */
	protected Token getTokenFromCookie(HttpServletRequest request) {
		// 从请求中获取当前请求(先尝试从拦截器的request中获取已解密的Token,如果有直接获取,如果为空,则重新解密获取Token)
		Token tk = this.attrToken(request);
		if (tk == null) {
			tk = this.getToken(request, config.getEncrypt(), config.getCookieName());
		}
		return tk;
	}


	/* ************************** 登录/登出/统计相关处理方法 ************************** */
	/**
	 * @desc 根据Token生成登录信息Cookie
	 * @param request http请求对象
	 * @param token   SSO登录信息票据
	 * @param encrypt 对称加密算法类
	 * @return Cookie 登录信息Cookie {@link Cookie}
	 */
	private Cookie generateCookie(HttpServletRequest request, Token token, ISSOEncrypt encrypt) {
		try {
			Cookie cookie = new Cookie(config.getCookieName(), encryptCookie(request, token, encrypt));
			cookie.setPath(config.getCookiePath());
			cookie.setSecure(config.getCookieSecure());

			// 获取 Cookie 所在域名(有些浏览器 localhost 可能无法设置 cookie)
			String domain = config.getCookieDomain();
			cookie.setDomain(domain);

			// 设置Cookie超时时间
			int maxAge = config.getCookieMaxage();
			Integer attrMaxAge = (Integer) request.getAttribute(SSOConfig.SSO_COOKIE_MAXAGE);
			if (attrMaxAge != null) {
				maxAge = attrMaxAge;
			}
			if (maxAge >= 0) {
				cookie.setMaxAge(maxAge);
			}
			return cookie;
		} catch (Exception e) {
			logger.error("根据Token生成登录信息Cookie失败[" + e.toString() + "]");
			return null;
		}
	}

	/**
	 * @desc 加密Token信息
	 * @param request http请求对象
	 * @param token   SSO 登录信息票据
	 * @param encrypt 对称加密算法类
	 * @return Cookie 登录信息Cookie {@link Cookie}
	 */
	protected String encryptCookie(HttpServletRequest request, Token token, ISSOEncrypt encrypt) throws Exception {
		if (token == null) {
			throw new SsoException("进行加密处理的Token不能为空!");
		}

		// token加密混淆
		String jt = token.jsonToken();

		StringBuilder sb = new StringBuilder();
		sb.append(jt).append(SSOConfig.CUT_SYMBOL);

		// 判断是否认证浏览器信息 否则取8位随机数混淆
		if (config.getCookieBrowser()) {
			// 混淆浏览器版本信息,获取浏览器客户端信息签名值
			sb.append(BrowserUtil.getUserAgent(request, jt));
		} else {
			sb.append(RandomUtil.getCharacterAndNumber(8));
		}
		return encrypt.encrypt(sb.toString(), config.getSecretkey());
	}

	/* ************************** 客户端处理Token相关方法 ************************** */
	/**
	 * @desc 获取当前请求 Token
	 * @param request http请求对象
	 * @param encrypt    对称加密算法类
	 * @param cookieName Cookie名称
	 * @return Token ${Token}
	 */
	private Token getToken(HttpServletRequest request, ISSOEncrypt encrypt, String cookieName) {
		String jsonToken = this.getJsonToken(request, encrypt, cookieName);

		if(StringUtils.isBlank(jsonToken)) {
			/* 未登录请求 */
			logger.info("当前请求中的JsonToken为空!");
			return null;
		} else {
			return config.getToken().parseToken(jsonToken);
		}
	}

	/**
	 * 判断 Token 是否缓存,如果缓存不存在则退出登录
	 * @param request http请求对象
	 * @param encrypt 对称加密算法类
	 * @param cache   SSO缓存对象
	 * @return Token {@link Token}
	 * @desc Token 是否缓存处理逻辑
	 */
	private Token cacheToken(HttpServletRequest request, ISSOEncrypt encrypt, ISSOCache cache) {
		// 如果缓存不存在退出登录
		if (cache != null) {
			Token cookieToken = getTokenFromCookie(request);
			if (cookieToken == null) {
				// 未登录
				return null;
			}

			Token cacheToken = cache.get(cookieToken.toCacheKey(), config.getCacheExpires());
			if (cacheToken == null) {
				// 开启缓存且失效, 返回 null 清除 Cookie 退出
				logger.info("得到的缓存token为空.");
				return null;
			} else {
				// 开启缓存,判断是否宕机：(1、缓存正常,返回 token; 2、缓存宕机,执行读取 Cookie 逻辑)
				if (cacheToken.getFlag() != TokenConstant.FLAG_CACHE_SHUT) {
					// 验证 cookie 与 cache 中 token 登录时间是否;不一致返回 null
					if (cookieToken.getTime() == cacheToken.getTime()) {
						return cacheToken;
					} else {
						logger.info("登录时间不一致或者被踢出登录,请重新登录!");
						request.setAttribute(SSOConfig.SSO_KICK_FLAG, SSOConfig.SSO_KICK_USER);
						return null;
					}
				}
			}
		}

		// Token 为 null 执行以下逻辑
		return getToken(request, encrypt, config.getCookieName());
	}
}